package com.ship.core.processor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.util.UrlPathHelper;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.*;

/**
 * ResponseBody写
 */
public class ResponseBodyWrite {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private List<MediaType> allSupportedMediaTypes;

    private static final Set<String> WHITELISTED_MEDIA_BASE_TYPES = new HashSet<String>(Arrays.asList("audio", "image", "video"));

    private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application");

    private static final UrlPathHelper DECODING_URL_PATH_HELPER = new UrlPathHelper();

    private static final UrlPathHelper RAW_URL_PATH_HELPER = new UrlPathHelper();

    private List<HttpMessageConverter<?>> messageConverters;

    private MyRequestResponseBodyAdviceChain advice;

    private ContentNegotiationManager contentNegotiationManager;

    private Set<String> safeExtensions;

    private PathExtensionContentNegotiationStrategy pathStrategy;


    public ResponseBodyWrite(List<MediaType> allSupportedMediaTypes, List<HttpMessageConverter<?>> messageConverters, MyRequestResponseBodyAdviceChain advice, ContentNegotiationManager contentNegotiationManager
            , Set<String> safeExtensions, PathExtensionContentNegotiationStrategy pathStrategy) {
        this.allSupportedMediaTypes = allSupportedMediaTypes;
        this.messageConverters = messageConverters;
        this.advice = advice;
        this.contentNegotiationManager = contentNegotiationManager;
        this.safeExtensions = safeExtensions;
        this.pathStrategy = pathStrategy;
    }

    protected <T> void writeWithMessageConverters(T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
        Object outputValue;
        Class<?> valueType;
        Type declaredType;

        if (value instanceof CharSequence) {
            outputValue = value.toString();
            valueType = String.class;
            declaredType = String.class;
        } else {
            outputValue = value;
            valueType = getReturnValueType(outputValue, returnType);
            declaredType = getGenericType(returnType);
        }

        HttpServletRequest request = inputMessage.getServletRequest();
        List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
        List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);

        if (outputValue != null && producibleMediaTypes.isEmpty()) {
            throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
        }

        Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
        for (MediaType requestedType : requestedMediaTypes) {
            for (MediaType producibleType : producibleMediaTypes) {
                if (requestedType.isCompatibleWith(producibleType)) {
                    compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
                }
            }
        }
        if (compatibleMediaTypes.isEmpty()) {
            if (outputValue != null) {
                throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
            }
            return;
        }

        List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
        MediaType.sortBySpecificityAndQuality(mediaTypes);

        MediaType selectedMediaType = null;
        for (MediaType mediaType : mediaTypes) {
            if (mediaType.isConcrete()) {
                selectedMediaType = mediaType;
                break;
            } else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
                selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
                break;
            }
        }

        if (selectedMediaType != null) {
            selectedMediaType = selectedMediaType.removeQualityValue();
            for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
                if (messageConverter instanceof GsonHttpMessageConverter) {
                    if (((GenericHttpMessageConverter) messageConverter).canWrite(
                            declaredType, valueType, selectedMediaType)) {
                        outputValue = (T) advice.beforeBodyWrite(outputValue, returnType, selectedMediaType,
                                (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
                                inputMessage, outputMessage);
                        if (outputValue != null) {
                            addContentDispositionHeader(inputMessage, outputMessage);
                            ((GenericHttpMessageConverter) messageConverter).write(
                                    outputValue, declaredType, selectedMediaType, outputMessage);
                            if (logger.isDebugEnabled()) {
                                logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
                                        "\" using [" + messageConverter + "]");
                            }
                        }
                        return;
                    }
                }
            }
        }

        if (outputValue != null) {
            throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
        }
    }

    private Type getGenericType(MethodParameter returnType) {
        if (HttpEntity.class.isAssignableFrom(returnType.getParameterType())) {
            return ResolvableType.forType(returnType.getGenericParameterType()).getGeneric(0).getType();
        } else {
            return returnType.getGenericParameterType();
        }
    }

    private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
        List<MediaType> mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
        return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes);
    }

    private MediaType getMostSpecificMediaType(MediaType acceptType, MediaType produceType) {
        MediaType produceTypeToUse = produceType.copyQualityValue(acceptType);
        return (MediaType.SPECIFICITY_COMPARATOR.compare(acceptType, produceTypeToUse) <= 0 ? acceptType : produceTypeToUse);
    }


    private void addContentDispositionHeader(ServletServerHttpRequest request,
                                             ServletServerHttpResponse response) {

        HttpHeaders headers = response.getHeaders();
        if (headers.containsKey(HttpHeaders.CONTENT_DISPOSITION)) {
            return;
        }

        try {
            int status = response.getServletResponse().getStatus();
            if (status < 200 || status > 299) {
                return;
            }
        } catch (Throwable ex) {
            // Ignore
        }

        HttpServletRequest servletRequest = request.getServletRequest();
        String requestUri = RAW_URL_PATH_HELPER.getOriginatingRequestUri(servletRequest);

        int index = requestUri.lastIndexOf('/') + 1;
        String filename = requestUri.substring(index);
        String pathParams = "";

        index = filename.indexOf(';');
        if (index != -1) {
            pathParams = filename.substring(index);
            filename = filename.substring(0, index);
        }

        filename = DECODING_URL_PATH_HELPER.decodeRequestString(servletRequest, filename);
        String ext = StringUtils.getFilenameExtension(filename);

        pathParams = DECODING_URL_PATH_HELPER.decodeRequestString(servletRequest, pathParams);
        String extInPathParams = StringUtils.getFilenameExtension(pathParams);

        if (!safeExtension(servletRequest, ext) || !safeExtension(servletRequest, extInPathParams)) {
            headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=f.txt");
        }
    }


    private boolean safeExtension(HttpServletRequest request, String extension) {
        if (!StringUtils.hasText(extension)) {
            return true;
        }
        extension = extension.toLowerCase(Locale.ENGLISH);
        if (this.safeExtensions.contains(extension)) {
            return true;
        }
        String pattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
        if (pattern != null && pattern.endsWith("." + extension)) {
            return true;
        }
        if (extension.equals("html")) {
            String name = HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;
            Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(name);
            if (!CollectionUtils.isEmpty(mediaTypes) && mediaTypes.contains(MediaType.TEXT_HTML)) {
                return true;
            }
        }
        return safeMediaTypesForExtension(extension);
    }

    private boolean safeMediaTypesForExtension(String extension) {
        List<MediaType> mediaTypes = null;
        try {
            mediaTypes = this.pathStrategy.resolveMediaTypeKey(null, extension);
        } catch (HttpMediaTypeNotAcceptableException ex) {
            // Ignore
        }
        if (CollectionUtils.isEmpty(mediaTypes)) {
            return false;
        }
        for (MediaType mediaType : mediaTypes) {
            if (!safeMediaType(mediaType)) {
                return false;
            }
        }
        return true;
    }

    private boolean safeMediaType(MediaType mediaType) {
        return (WHITELISTED_MEDIA_BASE_TYPES.contains(mediaType.getType()) ||
                mediaType.getSubtype().endsWith("+xml"));
    }

    protected Class<?> getReturnValueType(Object value, MethodParameter returnType) {
        return (value != null ? value.getClass() : returnType.getParameterType());
    }

    protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {
        Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
        if (!CollectionUtils.isEmpty(mediaTypes)) {
            return new ArrayList<MediaType>(mediaTypes);
        } else if (!this.allSupportedMediaTypes.isEmpty()) {
            List<MediaType> result = new ArrayList<MediaType>();
            for (HttpMessageConverter<?> converter : this.messageConverters) {
                if (converter instanceof GenericHttpMessageConverter && declaredType != null) {
                    if (((GenericHttpMessageConverter<?>) converter).canWrite(declaredType, valueClass, null)) {
                        result.addAll(converter.getSupportedMediaTypes());
                    }
                } else if (converter.canWrite(valueClass, null)) {
                    result.addAll(converter.getSupportedMediaTypes());
                }
            }
            return result;
        } else {
            return Collections.singletonList(MediaType.ALL);
        }
    }
}
